;;; smartparens-ess.el --- Smartparens Extension for Emacs Speaks Statistics  -*- lexical-binding: t; -*-

;; Copyright (c) 2015-2016 Bernhard Pröll

;; Author: Bernhard Pröll
;; Maintainer: Bernhard Pröll
;; URL: https://github.com/Fuco1/smartparens
;; Created: 2015-02-26
;; Version: 0.2
;; Keywords: abbrev convenience editing

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; This file provides some additional configuration for ESS.  To use
;; it, simply add:
;;
;; (require 'smartparens-ess)
;;
;; into your configuration.  You can use this in conjunction with the
;; default config or your own configuration.
;;
;;; Code:

(require 'smartparens)
(require 'rx)

(defvar ess-roxy-str)

(declare-function ess-roxy-indent-on-newline "ess-roxy")


;; avoid traveling commas when slurping
;; (|a, b), c ---> (|a, b, c)
(dolist (mode '(ess-mode inferior-ess-mode))
  (add-to-list 'sp-sexp-suffix (list mode 'regexp "")))

;; `sp-sexp-prefix' for ESS
(add-to-list 'sp-sexp-prefix
             (list 'ess-mode 'regexp
                   (rx (zero-or-more (or word (syntax symbol))))))

;; slurping follows Google's R style guide
;; see https://google.github.io/styleguide/Rguide.xml
(defun sp-ess-pre-handler (_id action _context)
  "Remove spaces before opening parenthesis in a function call.
Remove redundant space around commas.
ID, ACTION, CONTEXT."
  (when (equal action 'slurp-forward)
    (let ((sxp (sp-get-thing 'back)))
      (save-excursion
        (goto-char (sp-get sxp :beg-prf))
        ;; (|)   x ---> (x)
        (when (looking-back (rx (syntax open-parenthesis)
                                (one-or-more space)) nil)
          (cycle-spacing 0 nil 'single-shot))
        (cond
         ;; (|)if(cond) ---> (|if (cond))
         ((member (sp-get sxp :prefix) '("if" "for" "while"))
          (goto-char (sp-get sxp :beg))
          (cycle-spacing 1 nil 'single-shot))
         ;; (|)v [,2] <- if(x > 1) ---> (v[,2] <- if (x > 1))
         ((and
           (member (sp-get sxp :op) '("[" "("))
           (equal (sp-get sxp :prefix) "")
           (looking-back
            (rx (and (not-char "%" ",")
                     (not (syntax close-parenthesis)))
                (one-or-more space)) nil)
           (not (member
                 (save-excursion
                   (sp-backward-sexp)
                   (thing-at-point 'word 'noprop))
                 '("if" "for" "while"))))
          (cycle-spacing 0 nil 'single-shot))
         ;; (|[...])%in% ---> ([...] %in%|)
         ((or (looking-at "%") (looking-back "%" nil))
          (just-one-space))
         ;; (|)a , b,    c ---> (|a, b, c)
         ((looking-back
           (rx (zero-or-more space) "," (zero-or-more space))
           (line-beginning-position) 'greedy)
          (replace-match ", "))))))
  (when (equal action 'slurp-backward)
    (let ((sxp (sp-get-thing)))
      (save-excursion
        (goto-char (sp-get sxp :end))
        ;; x  (|) ---> (x)
        (when (looking-at (rx (one-or-more space)
                              (syntax close-parenthesis)))
          (cycle-spacing 0 nil 'single-shot))
        ;; if(cond){} (|) ---> (if (cond) {}|)
        (cond ((member (sp-get sxp :prefix) '("if" "for" "while"))
               (goto-char (sp-get sxp :beg))
               (cycle-spacing 1 nil 'single-shot))
              ;; for style reasons there should be a space before curly
              ;; brackets and binary operators
              ((and (member (sp-get sxp :op) '("{" "%"))
                    (not (looking-at (rx (syntax close-parenthesis)))))
               (cycle-spacing 1 nil 'single-shot))
              ;; v[2](|) ---> (v[2]|)
              ((and
                (not (member (thing-at-point 'word 'noprop)
                             '("if" "for" "while")))
                (looking-at
                 (rx (and (zero-or-more space)
                          (not-char "{")
                          (or (syntax close-parenthesis)
                              (char "(")
                              (char "["))))))
               (cycle-spacing 0 nil 'single-shot))
              ;; 1 , 2 (|) ---> (1, 2)
              ((looking-at
                (rx (zero-or-more space) "," (zero-or-more space)))
               (replace-match ", ")))))))

;; function(x) {|} ---> function(x) {\n|\n}
;; ##' \tabular{rrr}{|} --->
;; ##' \tabular{rrr}{
;; ##'   |
;; ##' }
(defun sp-ess-open-sexp-indent (&rest _args)
  "Open new brace or bracket with indentation.
ARGS."
  (if (and (fboundp 'ess-roxy-entry-p) (ess-roxy-entry-p))
      (progn
        (save-excursion (ess-roxy-indent-on-newline))
        (when (looking-back ess-roxy-str nil)
          (cycle-spacing 3 nil t)))
    (newline)
    (indent-according-to-mode)
    (forward-line -1)
    (indent-according-to-mode)))

(defun sp-ess-roxy-str-p (_id action _context)
  "Test if looking back at `ess-roxy-re'.
ID, ACTION, CONTEXT."
  (when (and (boundp 'ess-roxy-re) (eq action 'insert))
    (sp--looking-back-p ess-roxy-re)))

(sp-with-modes 'ess-mode
  (sp-local-pair "{" nil
                 :pre-handlers '(sp-ess-pre-handler)
    ;; the more reasonable C-j interferes with default binding for
    ;; `ess-eval-line'
                 :post-handlers '((sp-ess-open-sexp-indent "M-j")))
  (sp-local-pair "(" nil
                 :pre-handlers '(sp-ess-pre-handler)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j")))
  (sp-local-pair "[" nil
                 :pre-handlers '(sp-ess-pre-handler)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j")))
  (sp-local-pair "'" nil
                 :unless '(sp-ess-roxy-str-p sp-in-comment-p sp-in-string-quotes-p)))

;;; roxygen2 markup
;; see https://cran.r-project.org/web/packages/roxygen2/vignettes/formatting.html
(sp-with-modes 'ess-mode
  (sp-local-pair "\\strong{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\strong")
  (sp-local-pair "\\emph{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\emph")
  (sp-local-pair "\\code{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\code")
  (sp-local-pair "\\url{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\url")
  (sp-local-pair "\\link{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\link")
  (sp-local-pair "\\href{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\href"
                 :suffix "{[^}]*}")
  (sp-local-pair "\\email{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\email")
  (sp-local-pair "\\pkg{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\pkg")
  (sp-local-pair "\\item{" "}"
                 :when '(sp-in-comment-p)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j"))
                 :trigger "\\item{")
  (sp-local-pair "\\enumerate{" "}"
                 :when '(sp-in-comment-p)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j"))
                 :trigger "\\enumerate")
  (sp-local-pair "\\itemize{" "}"
                 :when '(sp-in-comment-p)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j"))
                 :trigger "\\itemize")
  (sp-local-pair "\\describe{" "}"
                 :when '(sp-in-comment-p)
                 :post-handlers '((sp-ess-open-sexp-indent "M-j"))
                 :trigger "\\describe")
  (sp-local-pair "\\eqn{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\eqn")
  (sp-local-pair "\\deqn{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\deqn")
  (sp-local-pair "\\tabular{" "}"
                 :when '(sp-in-comment-p)
                 :trigger "\\tabular"
                 :post-handlers '((sp-ess-open-sexp-indent "M-j"))
                 :suffix "{[^}]*}"))


(provide 'smartparens-ess)
;;; smartparens-ess ends here
